backend/pkg/post/usecase/comment.go

456 lines
13 KiB
Go
Raw Normal View History

2025-11-19 09:06:44 +00:00
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)
}