456 lines
13 KiB
Go
456 lines
13 KiB
Go
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)
|
||
}
|
||
|