backend/pkg/post/usecase/comment.go

456 lines
13 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}