package usecase import ( "context" "errors" "fmt" "math" errs "backend/pkg/library/errors" "backend/pkg/post/domain/entity" "backend/pkg/post/domain/post" domainRepo "backend/pkg/post/domain/repository" domainUsecase "backend/pkg/post/domain/usecase" "backend/pkg/post/repository" "github.com/gocql/gocql" ) // CommentUseCaseParam 定義 CommentUseCase 的初始化參數 type CommentUseCaseParam struct { Comment domainRepo.CommentRepository Post domainRepo.PostRepository Like domainRepo.LikeRepository Logger errs.Logger } // CommentUseCase 實作 domain usecase 介面 type CommentUseCase struct { CommentUseCaseParam } // MustCommentUseCase 創建新的 CommentUseCase(如果失敗會 panic) func MustCommentUseCase(param CommentUseCaseParam) domainUsecase.CommentUseCase { return &CommentUseCase{ CommentUseCaseParam: param, } } // CreateComment 創建新評論 func (uc *CommentUseCase) CreateComment(ctx context.Context, req domainUsecase.CreateCommentRequest) (*domainUsecase.CommentResponse, error) { // 驗證輸入 if err := uc.validateCreateCommentRequest(req); err != nil { return nil, err } // 驗證貼文存在 var zeroUUID gocql.UUID if req.PostID == zeroUUID { return nil, errs.InputInvalidRangeError("post_id is required") } post, err := uc.Post.FindOne(ctx, req.PostID) if err != nil { if repository.IsNotFound(err) { return nil, errs.ResNotFoundError(fmt.Sprintf("post not found: %s", req.PostID)) } return nil, uc.handleDBError("Post.FindOne", req, err) } // 檢查貼文是否可見 if !post.IsVisible() { return nil, errs.ResNotFoundError("cannot comment on non-visible post") } // 建立評論實體 comment := &entity.Comment{ PostID: req.PostID, AuthorUID: req.AuthorUID, ParentID: req.ParentID, Content: req.Content, Status: post.CommentStatusPublished, } // 插入資料庫 if err := uc.Comment.Insert(ctx, comment); err != nil { return nil, uc.handleDBError("Comment.Insert", req, err) } // 如果是回覆,增加父評論的回覆數 if req.ParentID != nil { if err := uc.Comment.IncrementReplyCount(ctx, *req.ParentID); err != nil { uc.Logger.Error(fmt.Sprintf("failed to increment reply count: %v", err)) } } // 增加貼文的評論數 if err := uc.Post.IncrementCommentCount(ctx, req.PostID); err != nil { uc.Logger.Error(fmt.Sprintf("failed to increment comment count: %v", err)) } return uc.mapCommentToResponse(comment), nil } // GetComment 取得評論 func (uc *CommentUseCase) GetComment(ctx context.Context, req domainUsecase.GetCommentRequest) (*domainUsecase.CommentResponse, error) { // 驗證輸入 var zeroUUID gocql.UUID if req.CommentID == zeroUUID { return nil, errs.InputInvalidRangeError("comment_id is required") } // 查詢評論 comment, err := uc.Comment.FindOne(ctx, req.CommentID) if err != nil { if repository.IsNotFound(err) { return nil, errs.ResNotFoundError(fmt.Sprintf("comment not found: %s", req.CommentID)) } return nil, uc.handleDBError("Comment.FindOne", req, err) } return uc.mapCommentToResponse(comment), nil } // UpdateComment 更新評論 func (uc *CommentUseCase) UpdateComment(ctx context.Context, req domainUsecase.UpdateCommentRequest) (*domainUsecase.CommentResponse, error) { // 驗證輸入 var zeroUUID gocql.UUID if req.CommentID == zeroUUID { return nil, errs.InputInvalidRangeError("comment_id is required") } if req.AuthorUID == "" { return nil, errs.InputInvalidRangeError("author_uid is required") } if req.Content == "" { return nil, errs.InputInvalidRangeError("content is required") } // 查詢現有評論 comment, err := uc.Comment.FindOne(ctx, req.CommentID) if err != nil { if repository.IsNotFound(err) { return nil, errs.ResNotFoundError(fmt.Sprintf("comment not found: %s", req.CommentID)) } return nil, uc.handleDBError("Comment.FindOne", req, err) } // 驗證權限 if comment.AuthorUID != req.AuthorUID { return nil, errs.ResNotFoundError("not authorized to update this comment") } // 檢查是否可見 if !comment.IsVisible() { return nil, errs.ResNotFoundError("comment is not visible") } // 更新內容 comment.Content = req.Content // 更新資料庫 if err := uc.Comment.Update(ctx, comment); err != nil { return nil, uc.handleDBError("Comment.Update", req, err) } return uc.mapCommentToResponse(comment), nil } // DeleteComment 刪除評論(軟刪除) func (uc *CommentUseCase) DeleteComment(ctx context.Context, req domainUsecase.DeleteCommentRequest) error { // 驗證輸入 var zeroUUID gocql.UUID if req.CommentID == zeroUUID { return errs.InputInvalidRangeError("comment_id is required") } if req.AuthorUID == "" { return errs.InputInvalidRangeError("author_uid is required") } // 查詢評論 comment, err := uc.Comment.FindOne(ctx, req.CommentID) if err != nil { if repository.IsNotFound(err) { return errs.ResNotFoundError(fmt.Sprintf("comment not found: %s", req.CommentID)) } return uc.handleDBError("Comment.FindOne", req, err) } // 驗證權限 if comment.AuthorUID != req.AuthorUID { return errs.ResNotFoundError("not authorized to delete this comment") } // 刪除評論 if err := uc.Comment.Delete(ctx, req.CommentID); err != nil { return uc.handleDBError("Comment.Delete", req, err) } // 如果是回覆,減少父評論的回覆數 if comment.ParentID != nil { if err := uc.Comment.DecrementReplyCount(ctx, *comment.ParentID); err != nil { uc.Logger.Error(fmt.Sprintf("failed to decrement reply count: %v", err)) } } // 減少貼文的評論數 if err := uc.Post.DecrementCommentCount(ctx, comment.PostID); err != nil { uc.Logger.Error(fmt.Sprintf("failed to decrement comment count: %v", err)) } return nil } // ListComments 列出評論 func (uc *CommentUseCase) ListComments(ctx context.Context, req domainUsecase.ListCommentsRequest) (*domainUsecase.ListCommentsResponse, error) { // 驗證輸入 var zeroUUID gocql.UUID if req.PostID == zeroUUID { return nil, errs.InputInvalidRangeError("post_id is required") } if req.PageSize <= 0 { req.PageSize = 20 } if req.PageIndex <= 0 { req.PageIndex = 1 } // 構建查詢參數 params := &domainRepo.CommentQueryParams{ PostID: &req.PostID, ParentID: req.ParentID, PageSize: req.PageSize, PageIndex: req.PageIndex, OrderBy: req.OrderBy, OrderDirection: req.OrderDirection, } // 如果 OrderBy 未指定,預設為 created_at if params.OrderBy == "" { params.OrderBy = "created_at" } // 如果 OrderDirection 未指定,預設為 ASC if params.OrderDirection == "" { params.OrderDirection = "ASC" } // 執行查詢 comments, total, err := uc.Comment.FindByPostID(ctx, req.PostID, params) if err != nil { return nil, uc.handleDBError("Comment.FindByPostID", req, err) } // 轉換為 Response responses := make([]domainUsecase.CommentResponse, len(comments)) for i, c := range comments { responses[i] = *uc.mapCommentToResponse(c) } return &domainUsecase.ListCommentsResponse{ Data: responses, Page: domainUsecase.Pager{ PageIndex: req.PageIndex, PageSize: req.PageSize, Total: total, TotalPage: calculateTotalPages(total, req.PageSize), }, }, nil } // ListReplies 列出回覆 func (uc *CommentUseCase) ListReplies(ctx context.Context, req domainUsecase.ListRepliesRequest) (*domainUsecase.ListCommentsResponse, error) { // 驗證輸入 var zeroUUID gocql.UUID if req.CommentID == zeroUUID { return nil, errs.InputInvalidRangeError("comment_id is required") } if req.PageSize <= 0 { req.PageSize = 20 } if req.PageIndex <= 0 { req.PageIndex = 1 } // 構建查詢參數 params := &domainRepo.CommentQueryParams{ PageSize: req.PageSize, PageIndex: req.PageIndex, OrderBy: "created_at", OrderDirection: "ASC", } // 執行查詢 comments, total, err := uc.Comment.FindReplies(ctx, req.CommentID, params) if err != nil { return nil, uc.handleDBError("Comment.FindReplies", req, err) } // 轉換為 Response responses := make([]domainUsecase.CommentResponse, len(comments)) for i, c := range comments { responses[i] = *uc.mapCommentToResponse(c) } return &domainUsecase.ListCommentsResponse{ Data: responses, Page: domainUsecase.Pager{ PageIndex: req.PageIndex, PageSize: req.PageSize, Total: total, TotalPage: calculateTotalPages(total, req.PageSize), }, }, nil } // ListCommentsByAuthor 根據作者列出評論 func (uc *CommentUseCase) ListCommentsByAuthor(ctx context.Context, req domainUsecase.ListCommentsByAuthorRequest) (*domainUsecase.ListCommentsResponse, error) { if req.AuthorUID == "" { return nil, errs.InputInvalidRangeError("author_uid is required") } if req.PageSize <= 0 { req.PageSize = 20 } if req.PageIndex <= 0 { req.PageIndex = 1 } params := &domainRepo.CommentQueryParams{ PageSize: req.PageSize, PageIndex: req.PageIndex, OrderBy: "created_at", OrderDirection: "DESC", } comments, total, err := uc.Comment.FindByAuthorUID(ctx, req.AuthorUID, params) if err != nil { return nil, uc.handleDBError("Comment.FindByAuthorUID", req, err) } responses := make([]domainUsecase.CommentResponse, len(comments)) for i, c := range comments { responses[i] = *uc.mapCommentToResponse(c) } return &domainUsecase.ListCommentsResponse{ Data: responses, Page: domainUsecase.Pager{ PageIndex: req.PageIndex, PageSize: req.PageSize, Total: total, TotalPage: calculateTotalPages(total, req.PageSize), }, }, nil } // LikeComment 按讚評論 func (uc *CommentUseCase) LikeComment(ctx context.Context, req domainUsecase.LikeCommentRequest) error { // 驗證輸入 var zeroUUID gocql.UUID if req.CommentID == zeroUUID { return errs.InputInvalidRangeError("comment_id is required") } if req.UserUID == "" { return errs.InputInvalidRangeError("user_uid is required") } // 檢查是否已經按讚 existingLike, err := uc.Like.FindByTargetAndUser(ctx, req.CommentID, req.UserUID, "comment") if err == nil && existingLike != nil { // 已經按讚,直接返回成功 return nil } if err != nil && !repository.IsNotFound(err) { return uc.handleDBError("Like.FindByTargetAndUser", req, err) } // 建立按讚記錄 like := &entity.Like{ TargetID: req.CommentID, UserUID: req.UserUID, TargetType: "comment", } if err := uc.Like.Insert(ctx, like); err != nil { return uc.handleDBError("Like.Insert", req, err) } // 增加評論的按讚數 if err := uc.Comment.IncrementLikeCount(ctx, req.CommentID); err != nil { uc.Logger.Error(fmt.Sprintf("failed to increment like count: %v", err)) } return nil } // UnlikeComment 取消按讚評論 func (uc *CommentUseCase) UnlikeComment(ctx context.Context, req domainUsecase.UnlikeCommentRequest) error { // 驗證輸入 var zeroUUID gocql.UUID if req.CommentID == zeroUUID { return errs.InputInvalidRangeError("comment_id is required") } if req.UserUID == "" { return errs.InputInvalidRangeError("user_uid is required") } // 刪除按讚記錄 if err := uc.Like.DeleteByTargetAndUser(ctx, req.CommentID, req.UserUID, "comment"); err != nil { if repository.IsNotFound(err) { // 已經取消按讚,直接返回成功 return nil } return uc.handleDBError("Like.DeleteByTargetAndUser", req, err) } // 減少評論的按讚數 if err := uc.Comment.DecrementLikeCount(ctx, req.CommentID); err != nil { uc.Logger.Error(fmt.Sprintf("failed to decrement like count: %v", err)) } return nil } // validateCreateCommentRequest 驗證建立評論請求 func (uc *CommentUseCase) validateCreateCommentRequest(req domainUsecase.CreateCommentRequest) error { var zeroUUID gocql.UUID if req.PostID == zeroUUID { return errs.InputInvalidRangeError("post_id is required") } if req.AuthorUID == "" { return errs.InputInvalidRangeError("author_uid is required") } if req.Content == "" { return errs.InputInvalidRangeError("content is required") } return nil } // mapCommentToResponse 將 Comment 實體轉換為 CommentResponse func (uc *CommentUseCase) mapCommentToResponse(comment *entity.Comment) *domainUsecase.CommentResponse { return &domainUsecase.CommentResponse{ ID: comment.ID, PostID: comment.PostID, AuthorUID: comment.AuthorUID, ParentID: comment.ParentID, Content: comment.Content, Status: comment.Status, LikeCount: comment.LikeCount, ReplyCount: comment.ReplyCount, CreatedAt: comment.CreatedAt, UpdatedAt: comment.UpdatedAt, } } // handleDBError 處理資料庫錯誤 func (uc *CommentUseCase) handleDBError(funcName string, req any, err error) error { return errs.DBErrorErrorL( uc.Logger, []errs.LogField{ {Key: "func", Val: funcName}, {Key: "req", Val: req}, {Key: "error", Val: err.Error()}, }, fmt.Sprintf("database operation failed: %s", funcName), ).Wrap(err) }